/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.jpxx.workflow;

import java.io.Serializable;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Vector;

import org.jpxx.workflow.rule.Rule;

/**
 * The Node class represents a node in a directed graph. A directed graph is
 * used to represent a workflow model.
 */
public class Node implements Serializable {

	/**
	 * 
	 */
	private static final long serialVersionUID = 6608803012001130249L;

	private int nodeId;
	private String nodeType;
	private String name;
	private String description;
	private Vector<Destination> destinations;
	private Vector<Node> fromNodes;
	private Map<String, Object> properties;
	private boolean validated = false;

	static int count = 0;

	/**
	 * Constructs a new node
	 * 
	 * @param String
	 *            nodeName
	 */
	public Node(String nodeName, String nodeType) {
		this.name = nodeName;
		this.nodeType = nodeType;
		destinations = new Vector<Destination>();
		fromNodes = new Vector<Node>();
		description = nodeName;
		properties = new HashMap<String, Object>();
	}

	/**
	 * Constructs a new node
	 * 
	 * @param int nodeId
	 */
	public Node(int nodeId) {
		this.nodeId = nodeId;
		destinations = new Vector<Destination>();
		fromNodes = new Vector<Node>();
		properties = new HashMap<String, Object>();
	}

	/**
	 * Returns the node ID
	 * 
	 * @return int nodeId
	 */
	public int getNodeId() {
		return nodeId;
	}

	/**
	 * Returns the node ID
	 * 
	 * @return Integer nodeId
	 */
	public Integer getNodeIdAsInteger() {
		return new Integer(nodeId);
	}

	/**
	 * Returns the node name
	 * 
	 * @return String node name
	 */
	public String getNodeName() {
		return name;
	}

	/**
	 * Returns the node type
	 * 
	 * @return String node type
	 */
	public String getNodeType() {
		return nodeType;
	}

	/**
	 * Returns node description
	 * 
	 * @return description
	 */
	public String getDescription() {
		return description;
	}

	/**
	 * Sets the node description
	 * 
	 * @param String
	 *            d
	 */
	public void setDescription(String d) {
		description = d;
	}

	/**
	 * Returns the node's containee graph name. Only valid if node's type is
	 * CONTAINER
	 * 
	 * @return containee
	 */
	public String getContainee() {
		String containee = null;
		if (nodeType.equals(NodeType.CONTAINER.getType())) {
			containee = (String) properties.get("containee");
		}
		return containee;
	}

	/**
	 * Sets the node's containee graph name. Only valid if node's type is
	 * CONTAINER
	 * 
	 * @param String
	 *            grapghName
	 */
	public void setContainee(String graphName) {
		if (nodeType.equals(NodeType.CONTAINER.getType())) {
			properties.put("containee", graphName);
		}
	}

	/**
	 * Returns the node's containee graph version. Only valid if node's type is
	 * CONTAINER
	 * 
	 * @return containeeVersion
	 */
	public int getContaineeVersion() {
		int version = -1;
		if (nodeType.equals(NodeType.CONTAINER.getType())) {
			Integer iObj = (Integer) properties.get("containeeVersion");
			if (iObj != null) {
				version = iObj.intValue();
			}
		}
		return version;
	}

	/**
	 * Sets the node's containee graph version. Only valid if node's type is
	 * CONTAINER
	 * 
	 * @param int version
	 */
	public void setContaineeVersion(int i) {
		if (nodeType.equals(NodeType.CONTAINER.getType())) {
			properties.put("containeeVersion", new Integer(i));
		}
	}

	/**
	 * Detects if the graph contains cycles.
	 * 
	 * @param HashMap
	 *            hashTable - contains the nodes already visited
	 * @param cycleDetected
	 *            - true if cycle has been detected.
	 * 
	 * @return boolean true if cycle detected.
	 * 
	 */
	private boolean detectCycle(Map<Integer, Node> hashTable,
			boolean cycleDetected) {

		// We have found a cycle - rewind the recursion
		if (cycleDetected) {
			return true;
		}

		for (int i = 0; i < destinations.size(); i++) {
			Destination d = (Destination) destinations.elementAt(i);
			Node destNode = d.getNode();
			Integer destNodeId = destNode.getNodeIdAsInteger();

			// Is destination node already in the list of nodes we came from?
			// If yes, we have a cycle.
			Node findNode = (Node) hashTable.get(destNodeId);
			if (findNode != null) {
				// We've got a cycle. Unwind
				System.out.println("Cycle detected. From Node: " + nodeId
						+ " To Node: " + destNodeId);
				cycleDetected = true;
				break; // Get out
			} else {
				// No cycle detected - continue the graph traversal
				hashTable.put(destNodeId, destNode);
				cycleDetected = destNode.detectCycle(hashTable, cycleDetected);
				hashTable.remove(destNodeId);
			}
		}

		return cycleDetected;
	}

	/**
	 * Detects if the graph contains cycles.
	 */
	public boolean detectCycle() {
		Map<Integer, Node> hashTable = new HashMap<Integer, Node>();

		Integer objKey = new Integer(nodeId);
		hashTable.put(objKey, this);
		boolean result = detectCycle(hashTable, false);
		return result;
	}

	/**
	 * Recursively traverses all the nodes of a graph. Useful for debugging.
	 */
	public void traverse() {
		this.print();
		if (destinations.size() == 0) {
			System.out.println("No more destinations for " + nodeId);
		}

		for (int i = 0; i < destinations.size(); i++) {
			Destination d = (Destination) destinations.elementAt(i);
			d.getNode().traverse();
		}
	}

	/**
	 * Prints out node id and description of node. Useful for debugging.
	 */
	public void print() {
		System.out.println("Node Id: " + nodeId + "\n" + "Node Name: " + name
				+ "\n" + "Description: " + description);
		Iterator<String> itr = properties.keySet().iterator();
		while (itr.hasNext()) {
			String key = (String) itr.next();
			Object value = properties.get(key);
			System.out.println(key + " = " + value);
		}

	}

	/**
	 * Finds a node within a graph
	 * 
	 * @param int nodeId The node ID for finding
	 * @param Node
	 *            result The result
	 * 
	 * @return Node the result, null if not found
	 */
	private Node getNode(int nodeId, Node result) {
		if (result != null) {
			return result;
		}

		if (this.nodeId == nodeId) {
			System.out.println("Found " + nodeId);
			result = this;
		} else {
			for (int i = 0; i < destinations.size(); i++) {
				Destination d = (Destination) destinations.elementAt(i);
				result = d.getNode().getNode(nodeId, result);
				if (result != null)
					break;
			}
		}

		return result;
	}

	/**
	 * Finds and returns a node within a graph given a node ID
	 * 
	 * @param int nodeId The node ID for finding
	 * @return Node the result, null if not found
	 */
	public Node getNode(int nodeId) {
		return getNode(nodeId, null);
	}

	/**
	 * Finds a node within a graph
	 * 
	 * @param String
	 *            name The node name for finding
	 * @param Node
	 *            result The result
	 * 
	 * @return Node the result, null if not found
	 */
	private Node getNode(String name, Node result) {
		if (result != null) {
			return result;
		}

		if (this.name.equals(name)) {
			System.out.println("Found " + name);
			result = this;
		} else {
			for (int i = 0; i < destinations.size(); i++) {
				Destination d = (Destination) destinations.elementAt(i);
				result = d.getNode().getNode(name, result);
				if (result != null)
					break;
			}
		}

		return result;
	}

	/**
	 * Finds and returns a node within a graph given a node name
	 * 
	 * @param String
	 *            name The node name for finding
	 * @return Node the result, null if not found
	 */
	public Node getNode(String name) {
		return getNode(name, null);
	}

	/**
	 * Adds a destination and a rule to evaluate a workflowobject's transition
	 * to this destination.
	 * 
	 * @param Node
	 *            node The destination node
	 * @param String
	 *            rule The rule for reaching this destination
	 * 
	 */
	@SuppressWarnings("rawtypes")
	public void addDestination(Node node, Rule rule) {
		Destination d = new Destination(node, rule);
		destinations.addElement(d);
		node.addFromNode(this);
	}

	/**
	 * @return Vector - this node's list of destinations
	 */
	public Vector<Destination> getDestinations() {
		return destinations;
	}

	/**
	 * Adds a fromNode to this node
	 * 
	 * @param Node
	 *            node The from node
	 * 
	 */
	public void addFromNode(Node n) {
		fromNodes.addElement(n);
	}

	/**
	 * @return Vector - this node's list of from nodes
	 */
	public Vector<Node> getFromNodes() {
		return fromNodes;
	}

	/**
	 * @return Vector - all descendant nodes of specified type
	 */
	public Vector<Node> getNodes(String nodeType) {
		Vector<Node> v = new Vector<Node>();
		Map<String, Node> map = new HashMap<String, Node>();
		getNodes(nodeType, map);

		Iterator<Node> itr = map.values().iterator();
		while (itr.hasNext()) {
			v.addElement(itr.next());
		}
		return v;
	}

	private void getNodes(String nType, Map<String, Node> map) {
		if (nodeType.equals(nType)) {
			map.put(name, this);
		}
		for (int i = 0; i < destinations.size(); i++) {
			Destination d = (Destination) destinations.elementAt(i);
			Node dnode = d.getNode();
			dnode.getNodes(nType, map);
		}
	}

	/**
	 * @return Vector - all descendant nodes
	 */
	public Vector<Node> getNodes() {
		Vector<Node> v = new Vector<Node>();
		HashMap<String, Node> map = new HashMap<String, Node>();
		getNodes(map);

		Iterator<Node> itr = map.values().iterator();
		while (itr.hasNext()) {
			v.addElement(itr.next());
		}
		return v;
	}

	private void getNodes(Map<String, Node> map) {
		map.put(name, this);
		for (int i = 0; i < destinations.size(); i++) {
			Destination d = (Destination) destinations.elementAt(i);
			Node dnode = d.getNode();
			dnode.getNodes(map);
		}
	}

	/**
	 * Sets a property on a node
	 * 
	 * @param key
	 *            the property name
	 * @param value
	 *            the property value - must be serializable
	 * 
	 */
	public void setProperty(String key, Object value) {
		properties.put(key, value);
	}

	/**
	 * Gets a node's property
	 * 
	 * @param key
	 *            the property name
	 * @return the property value
	 */
	public Object getProperty(String key) {
		return properties.get(key);
	}

	/**
	 * Sets the timeout value for a Process node
	 * 
	 * @param timeoutMinutes
	 *            the timeout in minutes
	 * 
	 */
	public void setTimeoutMinutes(int tout) {
		if (!nodeType.equals(NodeType.PROCESS.getType())) {
			return;
		}
		properties.put("timeoutMinutes", new Integer(tout));
	}

	/**
	 * Gets the timeout for a Process node
	 * 
	 * @return the timeout in minutes
	 */
	public int getTimeoutMinutes() {
		if (!nodeType.equals(NodeType.PROCESS.getType())) {
			return -1;
		}
		Integer tout = (Integer) properties.get("timeoutMinutes");
		if (tout != null) {
			return tout.intValue();
		} else {
			return -1;
		}
	}

	/**
	 * Sets the timeout handler for a Process node
	 * 
	 * @param timeoutHandler
	 *            the name of the timeout handler (a workflow name)
	 * 
	 */
	public void setTimeoutHandler(String handler) {
		if (!nodeType.equals(NodeType.PROCESS.getType())) {
			return;
		}
		properties.put("timeoutHandler", handler);
	}

	/**
	 * Gets the timeout handler for a Process node
	 * 
	 * @return the timeout handler name
	 */
	public String getTimeoutHandler() {
		if (!nodeType.equals(NodeType.PROCESS.getType())) {
			return null;
		}
		String handler = (String) properties.get("timeoutHandler");
		return handler;
	}

	public void setValidated() {
		validated = true;
	}

	public boolean isValidated() {
		return validated;
	}

	public String toString() {
		return "[" + this.name + "]" + super.toString();
	}
}
